/**
 * 
 */
package jnzb.control;

import java.io.IOException;
import java.util.ArrayList;
import java.util.Iterator;

import jnzb.control.events.InfoEventProcessor;
import jnzb.model.nzb.File;
import jnzb.model.nzb.NZB;
import jnzb.model.nzb.Segment;
import jnzb.nntp.NewsConnectException;
import jnzb.nntp.NewsProvider;

/**
 * Object responsible for downloading from usenet. Controls the connection.
 * Takes nzb file elements and downloads all segments
 * 
 * @author Rid
 * 
 */
public class Downloader implements Runnable {

	private Controller controller;

	private boolean continueProcessing = true;

	private boolean stopDownloading = false;

	private NZB currentNZB = null;

	private NewsProvider news;

	private NZBProcessor nzbProcessor;

	private boolean paused = false;

	public Object lock = new Object();

	private boolean shutdown = false;

	private InfoEventProcessor infoProcessor;

	private Configuration config;

	private long numKPerSMeasurements;
	private double totalKPerS;

	// This is the flag that indicates that the downloader process is stopped,
	// but not exited - used in the case of error
	private boolean stoppedByError = false;

	/**
	 * Objects that get sent the updated average speed
	 */
	private ArrayList<AverageSpeedListener> speedListeners;

	public Downloader(Controller controller, NZBProcessor nzbProcessor,
			InfoEventProcessor infoProcessor) {
		this.controller = controller;
		this.nzbProcessor = nzbProcessor;
		this.infoProcessor = infoProcessor;
		this.speedListeners = new ArrayList<AverageSpeedListener>();
		this.config = Configuration.getInstance();
		this.news = new NewsProvider(config.getAccount(), infoProcessor);
		// Kick off the download monitor
		new Thread(this).start();
	}

	/**
	 * Causes the downloader to exit
	 */
	public void exit() {
		// Will exit thread
		this.continueProcessing = false;
		// Will stop download
		this.stopDownloading = true;
		this.news.abortDownload();
		infoProcessor.debug("downloader.stop. Shutdown=" + shutdown);
		// If not downloading or paused we should be asleep so wake up
		if (!isDownloading() || this.paused == true) {
			this.paused = false;
			synchronized (this) {
				notifyAll();
			}
		}
	}

	/**
	 * Stops the current download. Will indicate that the NZB was interrupted
	 */
	public void stopDownloading() {
		// Will stop download
		this.stopDownloading = true;
		this.news.abortDownload();
		// Indicate that we've aborted
		this.currentNZB.setAllAttempted(false);
	}

	/**
	 * Start downloading an nzb
	 * 
	 * @param file
	 */
	public synchronized void startDownload(NZB nzb) {
		this.currentNZB = nzb;
		this.stopDownloading = false;
		notifyAll();
	}

	/**
	 * Refactored run method
	 */
	public synchronized void run() {
		while (continueProcessing) {
			if (currentNZB != null) {
				try {
					nzbProcessor.startNewNZB(currentNZB);
					this.stoppedByError = false;
					downloadFiles();
					// If not told to stop, indicate we tried to get it all
					if (!stopDownloading) {
						currentNZB.setAllAttempted(true);
						infoProcessor.debug("Download finished OK");
					} else {
						infoProcessor.debug("Download interrupted");
					}
				} catch (NewsConnectException e) {
					infoProcessor
							.error("Severe error connectng to news provider. Please try again.");
					this.stoppedByError = true;
				} catch (IOException e) {
					infoProcessor
							.error("Severe error creating work structure.  Please check directory permissions.");
					this.stoppedByError = true;
				}
				// Take a copy and set our current nzb to null before saying
				// we've finished downloading or other threads will think we are
				// currently downloading (sometimes - it's a nasty thread timing
				// bug!)
				NZB tempCurrentNZB = currentNZB;
				currentNZB = null;
				nzbProcessor.downloadFinished(tempCurrentNZB, stoppedByError,
						stopDownloading);

				// Reset the control flag that stops us downloading
				stopDownloading = false;

			} else {
				// Nothing to do, just wait until we do
				try {
					infoProcessor
							.debug("Downloader stopped downloading (sleep)");
					wait();
					infoProcessor.debug("Downloader awake");
				} catch (InterruptedException e) {
					// Just go around again
				}
			}
		}
		shutdown = true;
		infoProcessor.debug("downloader exiting");
	}

	/**
	 * Downloads the files in an NZB
	 */
	private void downloadFiles() {
		Iterator<File> files = currentNZB.getFiles().iterator();

		while (files.hasNext() && !this.stopDownloading) {
			File file = files.next();

			// If we havent already downloaded all the segments and we arent
			// ignoring it
			if (!file.allSegmentsDownloaded() && !file.isIgnore()) {

				downloadFile(file);

				// This will save progress after a whole file, or even if the
				// downloader stops because it's been told to
				this.nzbProcessor.checkProgress(currentNZB);
				if (!stopDownloading) {
					// TODO put in super method
					this.controller.getUiDelegate().setCurrentDownload("", 0);
				}
			} else {
				if (file.isIgnore()) {
					infoProcessor.info("Skipping " + file.getSubject());
				}
			}
			if (file.allSegmentsDownloaded() && !file.isDecoded()
					&& !file.isIgnore()) {
				nzbProcessor.readyToDecode(file);
			}
		}

	}

	/**
	 * Download an NZB file element. This contains segments which are the
	 * individual downloadable parts. This method updates the gui and is where
	 * the pause/start state is detected
	 */
	private void downloadFile(File currentFile) {
		// Use the current time as a tag for all the segments;
		long startTime = System.currentTimeMillis();

		controller.getUiDelegate().setCurrentDownload(currentFile.getSubject(),
				currentFile.getNumberOfSegments());

		// Iterate over the segments and gets from the appropriate group
		Iterator<Segment> segments = currentFile.getSegments().iterator();

		String segmentPath = config.getSegmentPath(currentFile.getRootNZB()
				.getFilename());

		String segmentFilename;
		infoProcessor.info("Downloading " + currentFile.getSubject());
		while (segments.hasNext() && !this.stopDownloading) {
			// Have this here in case we've exited while paused
			Segment segment = segments.next();
			controller.getUiDelegate().setCurrentSegmentSize(segment);
			controller.getUiDelegate().setCurrentFileProgress(segment);
			// We might be resuming, so only download if we havent already
			// tried
			if (!segment.hasBeenDownloaded()) {
				 // infoProcessor.debug("Downloading segment "
				 // + segment.getNumber() + " of "
				 // + currentFile.getNumberOfSegments());

				segment.setDownloadAttempted(true);
				segmentFilename = segmentPath + startTime + segment.getNumber()
						+ ".seg";

				// Get the segment, recording the time it took
				double kPerS = news.getSegment(segment.getMessageID(),
						currentFile.getGroups(), segmentFilename, controller);
				if (kPerS > -1) {
					segment.setDownloadedSegmentFilename(segmentFilename);

					// Add measurement to average speed calculation
					this.updateAverageSpeed(kPerS);

					// Update the average speed display
					controller.getUiDelegate().updateTimeLeft(
							currentFile.getRootNZB(), this.getAverageSpeed());

				} else {
					infoProcessor
							.debug("DownloadFile : could not download segment");

					// Couldnt download segment - not sure what we do here!
					// Leave like this and see what happens
				}

				controller.getUiDelegate().incrementNZBPercentage(currentNZB);
			}
		}

	}

	/**
	 * @return
	 */
	public boolean isDownloading() {
		// TODO do we need to add newProvider.connected=true too?
		return this.currentNZB != null;
	}

	public boolean isPaused() {
		return stopDownloading;
	}

	public void setPaused(boolean paused) {
		this.stopDownloading = paused;
	}

	public void togglePaused() {
		stopDownloading = !stopDownloading;
		if (!stopDownloading) {
			synchronized (this) {
				notifyAll();
			}
		}

	}

	public boolean isShutdown() {
		return shutdown;
	}

	public NZB getCurrentNZB() {
		return this.currentNZB;
	}

	/**
	 * Update the average speed measurement for this session
	 * 
	 * @param kPerS
	 */
	private void updateAverageSpeed(double kPerS) {
		this.numKPerSMeasurements++;
		this.totalKPerS += kPerS;
		notifySpeedListeners();
	}

	/**
	 * Returns the average speed while downloading for the whole session
	 * 
	 * @return
	 */
	public double getAverageSpeed() {
		return this.totalKPerS / this.numKPerSMeasurements;
	}

	/**
	 * TODO could be generic download data listener Register a listener that the
	 * updated average speed will be sent to
	 * 
	 * @param listener
	 */
	public void registerListener(AverageSpeedListener listener) {
		this.speedListeners.add(listener);
	}

	/**
	 * Inform any listeners of the latest average speed
	 */
	private void notifySpeedListeners() {
		double avgSpeed = this.getAverageSpeed();
		Iterator<AverageSpeedListener> i = speedListeners.iterator();
		while (i.hasNext()) {
			i.next().setAverageSpeed(avgSpeed);
		}
	}
}
